#include "http.h"
#include <string.h>
#include "string_helper.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include <esp_http_server.h>
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#define AP_WIFI_SSID "ESP32AP"
#define AP_WIFI_PASSWORD "esp32password"
#define AP_MAX_STA_CONN 5
#define AP_WIFI_CHANNEL 1
#define STA_CONNECT_TIMEOUT 10

bool station_connected;
static char http_buffer [512];
const int CONNECTED_BIT = BIT0;
static EventGroupHandle_t wifi_event_group;

static esp_err_t gui_handler(httpd_req_t *req)
{
    char html[2048] = "\
    <html>\
        <head>\
            <title>WiFi Connection</title>\
            <style>\
                body { background-color: #3e3e3e; font-family: Arial, Helvetica, Sans-Serif; Color: #ffffff; }\
            </style>\
        </head>\
        <body>\
            <h1>WiFi Connection</h1>\
            <p>Enter WiFi details:</p>\
            <input type='text' name='SSID' id = 'ssid'><br>\
            <input type='text' name='PASS' id = 'pass'><br>\
            <input type='button' value='CONNECT' id = 'connect' onClick = 'connect()'>\
            <script>\
                function connect(){\
                    var xhr = new XMLHttpRequest();\
                    xhr.open('POST', '/ssid', false);\
                    xhr.send(document.getElementById('ssid').value + '}');\
                    xhr.open('POST', '/pass', false);\
                    xhr.send(document.getElementById('pass').value + '}');\
                    xhr.open('GET', '/connect', false);\
                    xhr.send( null );\
                }\
            </script>\
        </body>\
    </html>";

    const char* resp_str = html;
    httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

static esp_err_t sta_handler(httpd_req_t *req)
{
    for (int i = 10; i >= 0; i--) 
    {
        ESP_LOGI("MAIN", "Restarting in %d seconds...", i);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    
    ESP_LOGI("MAIN", "Restarting now.");
    fflush(stdout);
    esp_restart();
}

esp_err_t ssid_handler(httpd_req_t *req)
{
    char content[100];

    size_t recv_size = req->content_len > 100 ? 100 : req->content_len;

    int ret = httpd_req_recv(req, content, recv_size);
    if (ret <= 0) 
    {
        if (ret == HTTPD_SOCK_ERR_TIMEOUT)
        {
            httpd_resp_send_408(req);
        }
        return ESP_FAIL;
    }

    char* strArr [2];
    split_string(strArr, content, "}", 2);
    char* ssid = strArr[0];
    
    ESP_LOGI("MAIN", "\n");
    ESP_LOGI("MAIN", "Opening Non-Volatile Storage (NVS) handle... ");
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open("storage", NVS_READWRITE, &nvs_handle);
    ESP_LOGI("MAIN", "Updating SSID in NVS ... ");
    
    err = nvs_set_str(nvs_handle, "ssid", ssid);
    if (err != ESP_OK)
    {
        ESP_LOGE("MAIN", "Failed to save SSID to NVS!");
    }
    else
    {
        ESP_LOGI("MAIN", "New SSID: %s", ssid);
    }
    
    ESP_LOGI("MAIN", "Committing updates in NVS ... ");
    
    err = nvs_commit(nvs_handle);
    if (err != ESP_OK)
    {
        ESP_LOGE("MAIN", "Failed to commit changes to NVS!");
    }
    
    nvs_close(nvs_handle);
    
    const char resp[] = "SSID Response";
    httpd_resp_send(req, resp, strlen(resp));
    return ESP_OK;
}

esp_err_t pass_handler(httpd_req_t *req)
{
    char content[100];
    size_t recv_size = req->content_len > 100 ? 100 : req->content_len;

    int ret = httpd_req_recv(req, content, recv_size);
    if (ret <= 0) 
    {  
        if (ret == HTTPD_SOCK_ERR_TIMEOUT)
        {
            httpd_resp_send_408(req);
        }
        return ESP_FAIL;
    }

    char* strArr [2];
    split_string(strArr, content, "}", 2);
    char* pass = strArr[0];

    ESP_LOGI("MAIN", "Opening Non-Volatile Storage (NVS) handle... ");
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open("storage", NVS_READWRITE, &nvs_handle);
    ESP_LOGI("MAIN", "Updating password in NVS ... ");
    
    err = nvs_set_str(nvs_handle, "password", pass);
    if (err != ESP_OK)
    {
        ESP_LOGE("MAIN", "Failed to save password to NVS!");
    }
    else
    {
        ESP_LOGI("MAIN", "New password: %s", pass);
    }
    
    ESP_LOGI("MAIN", "Committing updates in NVS ... ");
    
    err = nvs_commit(nvs_handle);
    if (err != ESP_OK)
    {
        ESP_LOGE("MAIN", "Failed to commit changes to NVS!");
    }
    
    nvs_close(nvs_handle);

    const char resp[] = "PASS Response";
    httpd_resp_send(req, resp, strlen(resp));
    return ESP_OK;
}

static const httpd_uri_t gui = {
    .uri       = "/gui",
    .method    = HTTP_GET,
    .handler   = gui_handler,
    .user_ctx  = NULL
};

static const httpd_uri_t ssid = {
    .uri       = "/ssid",
    .method    = HTTP_POST,
    .handler   = ssid_handler,
    .user_ctx  = NULL
};

static const httpd_uri_t pass = {
    .uri       = "/pass",
    .method    = HTTP_POST,
    .handler   = pass_handler,
    .user_ctx  = NULL
};

static const httpd_uri_t sta_connect = {
    .uri       = "/connect",
    .method    = HTTP_GET,
    .handler   = sta_handler,
    .user_ctx  = NULL
};

esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
    httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "404");
    return ESP_FAIL;
}

static httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;

    ESP_LOGI("MAIN", "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK)
    {
    	ESP_LOGI("MAIN", "Registering URI handlers");
        httpd_register_uri_handler(server, &gui);
        httpd_register_uri_handler(server, &ssid);
        httpd_register_uri_handler(server, &pass);
        httpd_register_uri_handler(server, &sta_connect);
        return server;
    }

    ESP_LOGI("MAIN", "Error starting server!");
    return NULL;
}

static void connect_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    httpd_handle_t* server = (httpd_handle_t*) arg;
    if (*server == NULL)
    {
        ESP_LOGI("MAIN", "Starting webserver");
        *server = start_webserver();
    }
}

static void http_req_loop(void *pvParameters)
{
    while(1)
    {
        if (station_connected)
        {
            http_params params = {
                REQ_SERVER,
                REQ_PORT,
                STA_REQUEST,
                http_buffer,
                512
            };

            if (http_request(params) == 0)
            {
                ESP_LOGI("MAIN", "HTTP Request failed!");
            }
            else
            {
                ESP_LOGI("MAIN", "%s", http_buffer);
            }  
        }
        else
        {
            ESP_LOGI("MAIN", "WiFi not connected!");
        }
        
        vTaskDelay(10000 / portTICK_PERIOD_MS);
    }
}

static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
    {
        ESP_LOGI("MAIN", "Retrying connection in 10 seconds...");
        vTaskDelay(10000 / portTICK_PERIOD_MS);
        esp_wifi_connect();
        xEventGroupClearBits(wifi_event_group, CONNECTED_BIT);
        station_connected = false;
    } 
    else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
    {
        ESP_LOGI("MAIN", "Connected to WiFi");
        xEventGroupSetBits(wifi_event_group, CONNECTED_BIT);
        station_connected = true;
    }
}

static void initialise_wifi(void)
{
    esp_log_level_set("wifi", ESP_LOG_WARN);
    static bool initialized = false;
    
    if (initialized) 
    {
        return;
    }
    
    ESP_ERROR_CHECK(esp_netif_init());
    wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    
    esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap();
    assert(ap_netif);
    
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);
    
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &event_handler, NULL) );
    ESP_ERROR_CHECK( esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL) );

    ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_NULL) );
    ESP_ERROR_CHECK( esp_wifi_start() );

    initialized = true;
}

static bool wifi_apsta(int timeout_ms)
{
    bool ssid_found = false;
    bool password_found = false;
    wifi_config_t ap_config = { 0 };
    strcpy((char*)ap_config.ap.ssid, AP_WIFI_SSID);
    strcpy((char*)ap_config.ap.password, AP_WIFI_PASSWORD);
    ap_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
    ap_config.ap.ssid_len = strlen(AP_WIFI_SSID);
    ap_config.ap.max_connection = AP_MAX_STA_CONN;
    ap_config.ap.channel = AP_WIFI_CHANNEL;

    if (strlen(AP_WIFI_PASSWORD) == 0) 
    {
        ap_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    wifi_config_t sta_config = { 0 };
    
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open("storage", NVS_READWRITE, &nvs_handle);

    if (err != ESP_OK)
    {
        ESP_LOGE("MAIN","Error (%s) opening NVS handle!", esp_err_to_name(err));
    }
    else
    {
        ESP_LOGI("MAIN", "Reading SSID from NVS ... ");
        size_t ssid_size = 32;
        err = nvs_get_str(nvs_handle, "ssid", (char*)sta_config.sta.ssid, &ssid_size);
        switch (err)
        {
            case ESP_OK:
                ESP_LOGI("MAIN", "SSID = %s", (char*)sta_config.sta.ssid);
                ssid_found = true;
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                ESP_LOGI("MAIN", "The value is not initialized yet!");
                break;
            default :
                ESP_LOGI("MAIN", "Error (%s) reading!", esp_err_to_name(err));
        }
        
        ESP_LOGI("MAIN", "Reading password from NVS ... ");
        size_t pass_size = 64;
        err = nvs_get_str(nvs_handle, "password", (char*)sta_config.sta.password, &pass_size);
        switch (err)
        {
            case ESP_OK:
                ESP_LOGI("MAIN", "Password = %s", (char*)sta_config.sta.password);
                password_found = true;
                break;
            case ESP_ERR_NVS_NOT_FOUND:
                ESP_LOGI("MAIN", "The value is not initialized yet!");
                break;
            default :
                ESP_LOGI("MAIN", "Error (%s) reading!", esp_err_to_name(err));
        }
    }
    
    nvs_close(nvs_handle);

    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_APSTA) );
    ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_AP, &ap_config) );
    ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &sta_config) );
    ESP_ERROR_CHECK( esp_wifi_start() );
    
    ESP_LOGI("MAIN", "WIFI_MODE_AP started.");
    ESP_LOGI("MAIN", "SSID:%s password:%s channel:%d", AP_WIFI_SSID, AP_WIFI_PASSWORD, AP_WIFI_CHANNEL);

    if (ssid_found && password_found)
    {
        ESP_ERROR_CHECK( esp_wifi_connect() );
    }
    
    int bits = xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT, pdFALSE, pdTRUE, timeout_ms / portTICK_PERIOD_MS);
    ESP_LOGI("MAIN", "bits=%x", bits);
    
    if (bits) 
    {
        ESP_LOGI("MAIN", "WiFi connected!");
        ESP_LOGI("MAIN", "SSID:%s password:%s", (char*)sta_config.sta.ssid, (char*)sta_config.sta.password);
    } 
    else 
    {
        ESP_LOGE("MAIN", "WiFi connection failed!");
        ESP_LOGE("MAIN", "SSID:%s password:%s", (char*)sta_config.sta.ssid, (char*)sta_config.sta.password);
    }
    
    return (bits & CONNECTED_BIT) != 0;
}

void app_main()
{
    static httpd_handle_t server = NULL;

    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) 
    {
        ESP_ERROR_CHECK( nvs_flash_erase() );
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);    
    
    initialise_wifi();

    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server));
    
    ESP_LOGW("MAIN", "Start APSTA Mode");
    wifi_apsta(STA_CONNECT_TIMEOUT * 1000);
    
    server = start_webserver();
    xTaskCreate(&http_req_loop, "http_req_loop", 4096, NULL, 5, NULL);
}
